Fix: Spring 7 Webflux NoSuchElementError on HttpHeaders#entrySet() - #4556
Fix: Spring 7 Webflux NoSuchElementError on HttpHeaders#entrySet() #4556mtomik wants to merge 17 commits into
Conversation
|
💚 CLA has been signed |
🤖 GitHub commentsJust comment with:
|
09b0d62 to
4fb8061
Compare
|
thanks for the review 👍 the failed build was due to incompatibility of that sub cancel with java 7. I reworked that using Instrumentation - |
4569cfe to
92bfe51
Compare
|
Tick the box to add this pull request to the merge queue (same as
|
92bfe51 to
1a4387b
Compare
|
run docs-build |
|
@mtomik are you able to complete the remaining suggested feedback, I was waiting for this to do a release but if it's not going to be completed soon I'll push this to the next release |
This reverts commit 42f3bdd0eb5c54392699d96e6d029cceec232587.
|
I found there one more issue. cancel of the subscription was called before collecting all the headers could be finished and that could cause this exception: so I fixed it with the correct order + added the same error handling that we got for status code there. @jackshirazi that should be all from me. if the review of my last changes might block your release, no problem. lets move it to another one then. |
|
I just checked the matching performance in the test standard output, and it's totally acceptable: This instrumentation uses 3 classes, and none of them is an outlier in the table above.
|
|
|
||
| @Override | ||
| public ElementMatcher<? super NamedElement> getTypeMatcherPreFilter() { | ||
| return nameContains("Subscriber").or(nameContains("Subscription")); |
There was a problem hiding this comment.
I think there are a bunch in reactor that don't use those in the names. Maybe add that as a special case?
| return nameContains("Subscriber").or(nameContains("Subscription")); | |
| return nameStartsWith("reactor.") | |
| .or(nameContains("Subscriber")) | |
| .or(nameContains("Subscription")); |
There was a problem hiding this comment.
Asking an LLM to find known implementations of Subscription yields the following list, so reactor. is a good prefix, we might also add io.reactivex. and io.smallrye.mutiny. but I'm definitely not familiar with any of those reactor frameworks. Here it would not hurt to have a few false positives rather than lots of false positives creating lots of useless overhead, so this should be fine as a first step, and we can refine further if needed.
reactor.core.publisher.FluxInterval$IntervalRunnable
reactor.core.publisher.FluxSwitchMap$SwitchMapInner
reactor.core.publisher.SinkManyUnicastNoBackpressure
io.reactivex.internal.operators.flowable.FlowableAmb$AmbCoordinator
io.reactivex.internal.operators.flowable.FlowablePublishMulticast$OutputCanceller
io.smallrye.mutiny.operators.multi.MultiFlatMapOp$FlatMapInner
io.smallrye.mutiny.operators.multi.MultiCombineLatestOp$CombineLatestCoordinator
io.smallrye.mutiny.operators.multi.MultiGroupByOp$State
io.smallrye.mutiny.operators.multi.MultiZipOp$ZipCoordinator
io.smallrye.mutiny.operators.multi.processors.UnicastProcessor
io.smallrye.mutiny.operators.multi.MultiOperatorProcessor
io.smallrye.mutiny.operators.multi.builders.IntervalMulti$IntervalRunnable
org.springframework.http.server.reactive.ChannelSendOperator$WriteBarrier
org.springframework.http.server.reactive.ChannelSendOperator$WriteCompletionBarrier
There was a problem hiding this comment.
one drawback of this additional name matching is increased instruction time spent. So I run the numbers again:
current filter
2026-09-04 00:03:18,209 [ForkJoinPool-2-worker-1] DEBUG co.elastic.apm.agent.bci.InstrumentationStatsLifecycleListener - Total time spent matching: 257,422,186ns
| Advice name | Type ns | Method ns |
| ExecutorInstrumentation$ExecutorRunnableInstrumentation | 46,278,675 | 5,203,226 |
| SubscriptionCancelInstrumentation | 17,952,170 | 11,488,096 |
| JakartaFilterInstrumentation | 24,123,333 | 0 |
OR starts with "reactor."
2026-09-03 23:59:18,358 [ForkJoinPool-2-worker-1] DEBUG co.elastic.apm.agent.bci.InstrumentationStatsLifecycleListener - Total time spent matching: 282,551,157ns
| Advice name | Type ns | Method ns |
| ExecutorInstrumentation$ExecutorRunnableInstrumentation | 48,814,752 | 6,779,428 |
| SubscriptionCancelInstrumentation | 42,561,353 | 8,489,875 |
| JakartaFilterInstrumentation | 23,363,543 | 0 |
| JakartaAsyncInstrumentation$JakartaStartAsyncInstrumentation | 20,613,139 | 0 |
OR all 3 starts with you mentioned
2026-09-04 00:02:17,662 [ForkJoinPool-2-worker-1] DEBUG co.elastic.apm.agent.bci.InstrumentationStatsLifecycleListener - Total time spent matching: 289,889,601ns
| Advice name | Type ns | Method ns |
| SubscriptionCancelInstrumentation | 47,053,567 | 14,306,418 |
| ExecutorInstrumentation$ExecutorRunnableInstrumentation | 48,076,345 | 5,605,395 |
| JakartaFilterInstrumentation | 24,638,337 | 0 |
| JakartaAsyncInstrumentation$JakartaStartAsyncInstrumentation | 22,838,079 | 0 |
if we want to optimize the filter even further - with something like just nameEndsWith("Subscriber") we cover most of them with just this overhead.
2026-09-04 00:12:48,681 [ForkJoinPool-2-worker-1] DEBUG co.elastic.apm.agent.bci.InstrumentationStatsLifecycleListener - Total time spent matching: 259,733,930ns
| Advice name | Type ns | Method ns |
| ExecutorInstrumentation$ExecutorRunnableInstrumentation | 62,550,712 | 5,650,035 |
| JakartaFilterInstrumentation | 24,774,004 | 0 |
| SubscriptionCancelInstrumentation | 14,498,788 | 9,413,708 |
| JakartaAsyncInstrumentation$JakartaStartAsyncInstrumentation | 20,837,849 | 0 |
the nameContains("Subscription") was there just to match it even when someone did his own implementation - bigger chance that we catch it
nameEndsWith("Subscriber") OR all 3 your package prefixes
2026-09-04 00:09:40,953 [ForkJoinPool-2-worker-1] DEBUG co.elastic.apm.agent.bci.InstrumentationStatsLifecycleListener - Total time spent matching: 281,649,260ns
| Advice name | Type ns | Method ns |
| SubscriptionCancelInstrumentation | 38,772,826 | 14,850,740 |
| ExecutorInstrumentation$ExecutorRunnableInstrumentation | 48,154,398 | 5,293,119 |
| JakartaAsyncInstrumentation$JakartaStartAsyncInstrumentation | 24,675,265 | 0 |
| JakartaFilterInstrumentation | 23,984,791 | 0 |
the thing is that if we miss some of them, we just rely on GC to clean it up later. so it just about finding that balance I guess 😄 but at the end, every one of these will be better than it was before, where we missed all of them
What does this PR do?
Since the original PR is stuck for some time,
I created another one with the same commits + a bit better solution for the core of this issue.
Also when I was running these newer tests I found one more issue:
Spring7ServerFunctionalInstrumentationTest#dispatchErrortest was failing due to TracedSubscriber not removing the reference of the subscription oncancel()fromcontextMap. so the solution might be theCancellationAwareSubscriptionthat is just the wrapper calling thediscardIf()after cancel.Is it possible that the issue was there even with older versions of reactor (from older Spring version), but now in that Spring 7 ( reactor 3.8.6 ) it was always failing.
Checklist